//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.DataLayer
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Reflection;
    using System.Security.Principal;

    /// <summary>
    /// Implements base functionality for providers that support the IStorageSecurity interface
    /// </summary>
    [Serializable]
    public abstract class ConnectionSecure : Connection, IDataLayerConnectionSecurity, IDataLayerSecurityFilter
    {
        /// <summary>
        /// WOrker conenction.
        /// </summary>
        private Connection worker;

        /// <summary>
        /// Current user credentials.
        /// </summary>
        private ISecurityPrincipal currentUser;

        /// <summary>
        /// Gets or sets the authentication token.
        /// </summary>
        /// <value>The authentication token.</value>
        public static IPrincipal AuthenticationToken { get; set; }

        #region Connection implementation
        /// <summary>
        /// Provider that actually do the storage heavy lifting
        /// </summary>
        /// <value>The worker.</value>
        protected Connection Worker { get { return worker; } }

        /// <summary>
        /// Initializes a new instance of the <see cref="ConnectionSecure"/> class.
        /// </summary>
        /// <param name="worker">The worker.</param>
        protected ConnectionSecure(IConnectionSecureWorker worker)
        {
            this.worker = (Connection)worker;
            worker.ConnectionSecure = this;
            SecurityHelper.AlternateSecurityLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            List<IDataLayerSecurityFilter> filters = SecurityHelper.GetSecurityFilters(this, this.worker);
            actionFilter = new FilterActionCalls(filters);
            beforeFilter = new FilterBeforeCalls(filters);
            afterFilter = new FilterAfterCalls(filters);
        }

        /// <summary>
        /// Initializes the <see cref="ConnectionSecure"/> class.
        /// </summary>
        static ConnectionSecure()
        {
            AuthenticationToken = new System.Security.Principal.WindowsPrincipal(
                System.Security.Principal.WindowsIdentity.GetCurrent());
        }

        /// <summary>
        /// Information about the provider
        /// </summary>
        public override string DisplayInfo
        {
            get { return Worker.DisplayInfo; }
        }

        /// <summary>
        /// Is the connection alive?
        /// </summary>
        public override bool Alive
        {
            get { return Worker.Alive; }
        }

        /// <summary>
        /// Opens the connection
        /// </summary>
        public override void Open()
        {
            Worker.Open();

            IDataLayerSecurityFilter security = (IDataLayerSecurityFilter)this;
            Exception filterException;

            SecurityAction actionResult = security.Actions.Authenticate(out filterException, ref currentUser);
            switch (actionResult)
            {
                case SecurityAction.Deny:
                    {
                        if (filterException != null)
                            throw new ConnectionFailure("Request denied", filterException);
                        else
                            throw new ConnectionFailure("Request denied", new System.Security.SecurityException());
                    }
                case SecurityAction.Allow:
                    {
                        if (filterException != null)
                            throw new ConnectionFailure("Authentication failure", filterException);
                        break;
                    }
                default:
                    throw new NotImplementedException("Unknown security action result");
            }
        }

        /// <summary>
        /// Closes the connection
        /// </summary>
        public override void Close()
        {
            Worker.Close();
        }

        /// <summary>
        /// Performs an action on an object stored (or to be stored) in the backend
        /// </summary>
        /// <param name="obj">Object type to operate on</param>
        /// <param name="action">Action to perform on the object</param>
        /// <param name="paramList">Data serialized from the object to be stored / used</param>
        public override void Object(Connection.Storage obj, Connection.Action action, ref List<Parameter> paramList)
        {
            IDataLayerSecurityFilter security = (IDataLayerSecurityFilter)this;
            Exception filterException;

            // Save original param list
            List<Parameter> paramListOrg = new List<Parameter>(paramList);

            // Perform the checks before call
            SecurityAction beforeResult = security.Before.Object(out filterException, ref obj, ref action, ref paramList);
            CommitResult(filterException, beforeResult);

            Worker.Object(obj, action, ref paramList);

            // Perform checks after call
            SecurityAction afterResult = security.After.Object(out filterException, ref obj, ref action, ref paramListOrg, ref paramList);
            CommitResult(filterException, afterResult);
        }

        /// <summary>
        /// Performs an action on an object stored (or to be stored) in the backend and returns
        /// results of the operation to the caller (e.g. ID of the object just created, time
        /// updated, etc)
        /// </summary>
        /// <param name="obj">Object type to operate on</param>
        /// <param name="action">Action to perform on the object</param>
        /// <param name="paramList">Data serialized from the object to be stored / used</param>
        /// <param name="creator">Delegate function to build objects from data</param>
        /// <param name="results">List of objects constructed using ObjectCreator function</param>
        public override void Object(Connection.Storage obj, Connection.Action action, ref List<Parameter> paramList, Connection.ObjectCreator creator, out List<IObject> results)
        {
            IDataLayerSecurityFilter security = (IDataLayerSecurityFilter)this;
            Exception filterException;

            // Perform the checks before call
            List<IObject> secResults = new List<IObject>();
            SecurityAction beforeResult = security.Before.Object(out filterException, ref obj, ref action, creator, ref secResults);
            CommitResult(filterException, beforeResult);

            Worker.Object(obj, action, ref paramList, creator, out results);

            if (secResults != null && secResults.Count > 0)
            {
                // Join real call with security calls
                if (results == null)
                    results = secResults;
                else
                    results.AddRange(secResults);
            }

            // Perform checks after call
            SecurityAction afterResult = security.After.Object(out filterException, ref obj, ref action, creator, ref results);
            CommitResult(filterException, afterResult);
        }

        /// <summary>
        /// Perfoms an action on a relationship between two objects
        /// </summary>
        /// <param name="action">Action to perform</param>
        /// <param name="obj1">First object in the relation</param>
        /// <param name="obj2">Second object in the relation</param>
        public override void Relation(Connection.Action action, IObject obj1, IObject obj2)
        {
            IDataLayerSecurityFilter security = (IDataLayerSecurityFilter)this;
            Exception filterException;

            // Perform the checks before call
            SecurityAction beforeResult = security.Before
                .Relation(out filterException, ref action, ref obj1, ref obj2);
            CommitResult(filterException, beforeResult);

            Worker.Relation(action, obj1, obj2);

            // Perform checks after call
            SecurityAction afterResult = security.After
                .Relation(out filterException, ref action, ref obj1, ref obj2);
            CommitResult(filterException, afterResult);
        }

        /// <summary>
        /// Perfoms an action on a relationship between two objects
        /// </summary>
        /// <param name="action">Action to perform</param>
        /// <param name="obj1">First object in the relation</param>
        /// <param name="obj2">Second type in the relation</param>
        /// <param name="revKey">Foreign key field</param>
        /// <param name="paramList"></param>
        public override void Relation(Connection.Action action, IObject obj1, Connection.Storage obj2, string revKey, ref List<Parameter> paramList)
        {
            IDataLayerSecurityFilter security = (IDataLayerSecurityFilter)this;
            Exception filterException;

            // Save original param list
            List<Parameter> paramListOrg = new List<Parameter>(paramList);

            // Perform the checks before call
            SecurityAction beforeResult = security.Before
                .Relation(out filterException, ref action, ref obj1, ref obj2, ref revKey, ref paramList);
            CommitResult(filterException, beforeResult);

            Worker.Relation(action, obj1, obj2, revKey, ref paramList);

            // Perform checks after call
            SecurityAction afterResult = security.After
                .Relation(out filterException, ref action, ref obj1, ref obj2, ref revKey, ref paramListOrg, ref paramList);
            CommitResult(filterException, afterResult);
        }

        /// <summary>
        /// Performs an action on a stored (or to be stored) relation in the backend and returns
        /// results of the operation to the caller.
        /// </summary>
        /// <param name="action">Action to perform</param>
        /// <param name="obj1">First object in the relation</param>
        /// <param name="obj2Type">Type of the second object in the relation</param>
        /// <param name="creator">Delegate function to build relationship object from data</param>
        /// <param name="results">List of objects constructed using ObjectCreator function</param>
        public override void Relation(Connection.Action action, IObject obj1, Connection.Storage obj2Type, Connection.ObjectCreator creator, out List<IObject> results)
        {
            IDataLayerSecurityFilter security = (IDataLayerSecurityFilter)this;
            Exception filterException;

            // Perform the checks before call
            List<IObject> secResults = new List<IObject>();
            SecurityAction beforeResult = security.Before
                .Relation(out filterException, ref action, ref obj1, ref obj2Type, creator, ref secResults);
            CommitResult(filterException, beforeResult);

            Worker.Relation(action, obj1, obj2Type, creator, out results);

            if (secResults != null && secResults.Count > 0)
            {
                // Join real call with security calls
                if (results == null)
                    results = secResults;
                else
                    results.AddRange(secResults);
            }

            // Perform checks after call
            SecurityAction afterResult = security.After
                .Relation(out filterException, ref action, ref obj1, ref obj2Type, creator, ref results);
            CommitResult(filterException, afterResult);
        }

        /// <summary>
        /// Performs an action on a stored (or to be stored) relation in the backend and returns
        /// results of the operation to the caller.
        /// </summary>
        /// <param name="action">Action to perform</param>
        /// <param name="obj1">First object in the relation</param>
        /// <param name="field">Name of the field to search on (for multiple relationships against the same entity)</param>
        /// <param name="obj2Type">Type of the second object in the relation</param>
        /// <param name="creator">Delegate function to build relationship object from data</param>
        /// <param name="results">List of objects constructed using ObjectCreator function</param>
        public override void Relation(Connection.Action action, IObject obj1, string field, Connection.Storage obj2Type, Connection.ObjectCreator creator, out List<IObject> results)
        {
            IDataLayerSecurityFilter security = (IDataLayerSecurityFilter)this;
            Exception filterException;

            // Perform the checks before call
            List<IObject> secResults = new List<IObject>();
            SecurityAction beforeResult = security.Before
                .Relation(out filterException, ref action, ref obj1, ref field, ref obj2Type, creator, ref secResults);
            CommitResult(filterException, beforeResult);

            Worker.Relation(action, obj1, field, obj2Type, creator, out results);

            if (secResults != null && secResults.Count > 0)
            {
                // Join real call with security calls
                if (results == null)
                    results = secResults;
                else
                    results.AddRange(secResults);
            }

            // Perform checks after call
            SecurityAction afterResult = security.After
                .Relation(out filterException, ref action, ref obj1, ref field, ref obj2Type, creator, ref results);
            CommitResult(filterException, afterResult);
        }

        /// <summary>
        /// Method to copy required parameters
        /// </summary>
        /// <param name="src"></param>
        protected override void CopyFrom(Connection src)
        {
            // Ignore:
        }

       

        #endregion

        #region IDataLayerSecurity Members

        private FilterActionCalls actionFilter;
        private FilterAfterCalls afterFilter;
        private FilterBeforeCalls beforeFilter;

        /// <summary>
        /// Gets the actions.
        /// </summary>
        /// <value>The actions.</value>
        IDataLayerSecurityActionCalls IDataLayerSecurityFilter.Actions
        {
            get { return actionFilter; }
        }

        /// <summary>
        /// Gets the before.
        /// </summary>
        /// <value>The before.</value>
        IDataLayerSecurityBeforeCalls IDataLayerSecurityFilter.Before
        {
            get { return beforeFilter; }
        }

        /// <summary>
        /// Gets the after.
        /// </summary>
        /// <value>The after.</value>
        IDataLayerSecurityAfterCalls IDataLayerSecurityFilter.After
        {
            get { return afterFilter; }
        }

        #endregion

        /// <summary>
        /// Gets the current user.
        /// </summary>
        /// <value>The current user.</value>
        public ISecurityPrincipal CurrentUser
        {
            get { return currentUser; }
        }

        /// <summary>
        /// Commits the result.
        /// </summary>
        /// <param name="filterException">The filter exception.</param>
        /// <param name="beforeResult">The before result.</param>
        private static void CommitResult(Exception filterException, SecurityAction beforeResult)
        {
            switch (beforeResult)
            {
                case SecurityAction.Deny:
                    {
                        if (filterException != null)
                            throw new System.Security.SecurityException("Request denied", filterException);
                        else
                            throw new System.Security.SecurityException("Request denied");
                    }
                case SecurityAction.Allow:
                    {
                        if (filterException != null)
                            throw filterException;
                        break;
                    }
                default:
                    throw new NotImplementedException("Unknown security action result");
            }
        }
    }
}
